/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.api.core;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

/**
 * Defines paged result of data selection, it is rather dynamic data window than
 * regular page, as it refers to the specified selection region based on the items before
 * and the page size, but not on the page number and page size.
 *
 * <p>Examples:
 *
 * <p>Regular page.<br>
 * For page input arguments {@code skipItems = 3}, {@code pageSize = 3} and {@code totalCount = 10}
 * the page is normalized and will refer to the second page which is(item4, item5, item6):
 * <pre>
 *     item1.                      <- previous page start      <- first page start
 *     item2.
 *     item3.                      <- previous page end        <- first page end
 *     item4. <- page start
 *     item5.
 *     item6. <- page end
 *     item7.                      <- next page start
 *     item8.
 *     item9.                      <- next page end
 *     item.10                                                  <- last page start/end
 * </pre>
 *
 * <ul>Result page:
 * <li>Non-empty</li>
 * <li>Contains 3 items(item4, item5, item6)</li>
 * <li>Has the previous page(item1, item2, item3)
 * <li>Has the next page(item7, item8, item9)</li>
 * </ul>
 *
 *
 * <p>Data window.<br>
 * For page input arguments {@code skipItems = 2}, {@code pageSize = 3} and {@code totalCount = 7}
 * the page will refer to the following data window:
 * <pre>
 *     item1.                   <- first page start
 *     item2.
 *     item3. <- page start     <- first page end
 *     item4.
 *     item5. <- page end
 *     item6.
 *     item7.                    <- last page start/end
 * </pre>
 *
 * <ul>Result page:
 * <li>Non-empty</li>
 * <li>Contains 3 items(item3, item4, item5)</li>
 * <li>Doesn't have the previous page(because page refers to the elements which
 * are partially present in the first page(item3) and the second page(item4, item5)</li>
 * <li>Doesn't have the next page(the reason is the same to previous statement)</li>
 * </ul>
 *
 * <p>Note that all {@code Page} instances perform reference
 * calculations based on the given {@code itemsBefore} and {@code pageSize}
 * values which means that implementor is responsible for providing correct bounds
 * and data management.
 *
 * <p>The instances of this class are <b>NOT thread safe</b>.
 *
 * @param <ITEM_T>
 *         the type of the page items
 * @author Yevhenii Voevodin
 */
public class Page<ITEM_T> {

    private final int          pageSize;
    private final long         itemsBefore;
    private final long         totalCount;
    private final List<ITEM_T> items;

    /**
     * Creates a new page.
     *
     * @param items
     *         page items
     * @param itemsBefore
     *         items count before this page
     * @param pageSize
     *         page size
     * @param totalCount
     *         count of all the items
     * @throws NullPointerException
     *         when {@code items} collection is null
     * @throws IllegalArgumentException
     *         when {@code itemsBefore} is negative
     * @throws IllegalArgumentException
     *         when {@code pageSize} is non-positive
     * @throws IllegalArgumentException
     *         when {@code totalCount} is negative
     */
    public Page(Collection<? extends ITEM_T> items, long itemsBefore, int pageSize, long totalCount) {
        requireNonNull(items, "Required non-null items");
        this.items = new ArrayList<>(items);
        checkArgument(itemsBefore >= 0, "Required non-negative value of items before");
        this.itemsBefore = itemsBefore;
        checkArgument(pageSize > 0, "Required positive value of page size");
        this.pageSize = pageSize;
        checkArgument(totalCount >= 0, "Required non-negative value of total items");
        this.totalCount = totalCount;
    }

    /** Returns true whether this page doesn't contain items, returns false if it does. */
    public boolean isEmpty() {
        return items.isEmpty();
    }

    /**
     * Returns true when the current page has the next page,
     * otherwise when the page is the last page false will be returned.
     */
    public boolean hasNextPage() {
        return getNumber() != -1 && itemsBefore + pageSize < totalCount;
    }

    /**
     * Returns true when this page has the previous page,
     * otherwise when the page is the first page false will be returned.
     */
    public boolean hasPreviousPage() {
        return getNumber() != -1 && itemsBefore != 0;
    }

    /**
     * Returns a reference to the next page.
     *
     * <p>Note: This method was designed to be used in couple with {@link #hasNextPage()}.
     * Returns reference to the next page even when {@link #hasNextPage()} returns false.
     */
    public PageRef getNextPageRef() {
        return new PageRef(itemsBefore + pageSize, pageSize);
    }

    /**
     * Returns a reference to the previous page.
     *
     * <p>Note: This method was designed to be used in couple with {@link #hasPreviousPage()}.
     * Returns reference to the first page when {@link #hasPreviousPage()} returns false.
     */
    public PageRef getPreviousPageRef() {
        final long skipItems = itemsBefore <= pageSize ? 0 : itemsBefore - pageSize;
        return new PageRef(skipItems, pageSize);
    }

    /** Returns the reference to the last page. */
    public PageRef getLastPageRef() {
        final long lastPageItems = totalCount % pageSize;
        if (lastPageItems == 0) {
            return new PageRef(totalCount <= pageSize ? 0 : totalCount - pageSize, pageSize);
        }
        return new PageRef(totalCount - lastPageItems, pageSize);
    }

    /** Returns the reference to the first page. */
    public PageRef getFirstPageRef() {
        return new PageRef(0, pageSize);
    }

    /**
     * Returns the size of the current page.
     *
     * <p>Returned value is always positive and greater or equal
     * to the value returned by the {@link #getItemsCount()} method.
     */
    public int getSize() {
        return pageSize;
    }

    /**
     * Returns page number starting from 1.
     *
     * <p>If the page is not regular page(it refers rather to the
     * data window than to the certain page(e.g. skip=2, pageSize=4))
     * then this method returns -1.
     */
    public long getNumber() {
        if (itemsBefore % pageSize != 0) {
            return -1;
        }
        return itemsBefore / pageSize + 1;
    }

    /**
     * Returns the size of the page items, returned value
     * may be equal to 0 when the page {@link #isEmpty() is empty},
     * the values is the same to {@code page.getItems().size()}.
     */
    public int getItemsCount() {
        return items.size();
    }

    /** Returns the count of all the items. */
    public long getTotalItemsCount() {
        return totalCount;
    }

    /**
     * Returns page items or an empty list when page doesn't contain items.
     *
     * <p>Note that returned instance is modifiable list and modification applied
     * on that list will affect the origin page result items, which allows
     * components to modify items before propagating page.
     */
    public List<ITEM_T> getItems() {
        return items;
    }

    /**
     * Gets the page items and maps them with given {@code mapper}.
     *
     * @param mapper
     *         items mapper
     * @param <R>
     *         the type of the result items
     * @return the list of mapped items
     */
    public <R> List<R> getItems(Function<? super ITEM_T, ? extends R> mapper) {
        requireNonNull(mapper, "Required non-null mapper for page items");
        return items.stream()
                    .map(mapper::apply)
                    .collect(toList());
    }

    /**
     * Fills the given collection with page items.
     * This method may be convenient when needed collection
     * different from the {@link List}.
     *
     * <p>The common example:
     * <pre>{@code
     *      Set<User> user = page.fill(new TreeSet<>(comparator));
     * }</pre>
     *
     * <p>Note that this method uses {@code container.addAll(items)}
     * so be aware of putting modifiable collection.
     *
     * @param container
     *         collection which is used to fill result into
     * @param <COL_T>
     *         collection type
     * @return given collection instance {@code container} with items filled
     * @throws NullPointerException
     *         when {@code container} is null
     */
    public <COL_T extends Collection<? super ITEM_T>> COL_T fill(COL_T container) {
        requireNonNull(container, "Required non-null items container");
        container.addAll(items);
        return container;
    }

    /** Represents page reference as a combination of {@code skipItems & pageSize}. */
    public static class PageRef {
        private final long skipItems;
        private final int  pageSize;

        private PageRef(long skipItems, int pageSize) {
            this.skipItems = skipItems;
            this.pageSize = pageSize;
        }

        public long getItemsBefore() {
            return skipItems;
        }

        public int getPageSize() {
            return pageSize;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof PageRef)) {
                return false;
            }
            final PageRef that = (PageRef)obj;
            return skipItems == that.skipItems && pageSize == that.pageSize;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 31 * hash + Long.hashCode(skipItems);
            hash = 31 * hash + pageSize;
            return hash;
        }
    }
}
